/*
 * Copyright (c)  2020,  DataPipeline
 * All rights reserved.
 *
 * Id:MetaDataManager.java   2020-07-18 15:23 wanglijun
 */
package cn.datacare.orm.manager.impl;

import cn.datacare.orm.concurrent.ThreadSafeCaseInsensitiveHashMap;
import cn.datacare.orm.connection.ConnectionSource;
import cn.datacare.orm.database.ColDesc;
import cn.datacare.orm.database.TableDesc;
import cn.datacare.orm.dialect.DialectType;
import cn.datacare.orm.exception.OrmCoreException;
import cn.datacare.orm.exception.OrmErrorCode;
import cn.datacare.orm.manager.Manager;
import cn.datacare.orm.util.Separator;
import cn.datacare.orm.util.StringUtils;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.Set;

import static cn.datacare.orm.manager.MetaDataConfig.*;


/**
 * <p>
 * Title: MetaDataManager
 * </p>
 * <p>
 * Description: MetaDataManager
 * </p>
 * <p>
 * Copyright: Copyright (c) 2020
 * </p>
 * <p>
 * Company/Department: DataPipeline
 * </p>
 *
 * @author wanglijun
 * @version 1.0
 * @since 2020-07-18 15:23
 */
public class MetaDataManager {
    /***
     * $NOT_EXIST:{@value}
     */
    public static final String  STR_NOT_EXIST = "$NOT_EXIST";
    /**
     * 是否检查列是否自增，目前通过异常判断驱动不支持
     */
    boolean checkAuto = true;
    /**
     * 真表-假表，用于可能的分表分库，比如user表不存在，user001,user00存在
     * 因此user001->user
     */

    ThreadSafeCaseInsensitiveHashMap tableVirtualMap = new ThreadSafeCaseInsensitiveHashMap();

    private ConnectionSource connectionSource;

    private ThreadSafeCaseInsensitiveHashMap map = null;

    private final TableDesc NOT_EXIST = new TableDesc(STR_NOT_EXIST, StringUtils.EMPTY);

    private Manager sqlManager;

    private String defaultSchema;

    private String defaultCatalog;

    private String dialectType;

    public MetaDataManager(ConnectionSource connectionSource, Manager manager) {
        super();
        this.connectionSource = connectionSource;
        this.sqlManager = manager;
        this.dialectType = sqlManager.getDialectType().getName();
        //获取数据库Schema
        initDefaultSchema();

    }


    public ConnectionSource getConnectionSource() {
        return connectionSource;
    }

    public void setConnectionSource(ConnectionSource connectionSource) {
        this.connectionSource = connectionSource;
    }

    /***
     * 表是否在数据库中
     *
     * @param tableName
     * @return
     */
    public boolean existTable(String tableName) {
        TableDesc t = getTable(tableName);
        return t != null;
    }


    public TableDesc getTable(String name) {
        TableDesc table = getTableFromMap(name);
        if (table == null) {
            throw new OrmCoreException(OrmErrorCode.TABLE_NOT_EXIST, name);
        }

        if (table.getCols().size() == 0) {
            table = initTable(table);
        }
        return table;
    }

    public Set<String> allTable() {
        if (map == null) {
            this.initMetadata();
        }
        return this.map.keySet();
    }

    public void refresh() {
        map = null;
        this.initMetadata();
    }


    private TableDesc getTableFromMap(String tableName) {
        TableDesc desc = null;
        if (map == null) {
            synchronized (this) {
                if (map != null) {
                    desc = (TableDesc) map.get(tableName);
                } else {
                    this.initMetadata();
                    desc = (TableDesc) map.get(tableName);
                }

            }
        } else {
            desc = (TableDesc) map.get(tableName);
        }

        if (desc == NOT_EXIST) {
            return null;
        } else if (desc == null) {
            int index = tableName.indexOf(Separator.DOT);
            if (index != -1) {
                //
                String schema = tableName.substring(0, index);
                String table = tableName.substring(index + 1);
                return initOtherSchemaTable(schema, table);

            } else {
                return null;
            }

        } else {
            return desc;
        }
    }

    private TableDesc initTable(TableDesc desc) {

        synchronized (desc) {

            if (!desc.getCols().isEmpty()) {
                return desc;
            }
            String tableName = desc.getRealTableName() != null ? desc.getRealTableName() : desc.getName();

            try(Connection conn=this.connectionSource.getMetaData()){
                String catalog = desc.getCatalog();
                String schema = desc.getSchema();
                schema = this.getDbSchema(schema);
                DatabaseMetaData metaData = conn.getMetaData();

                try( ResultSet rs = metaData.getPrimaryKeys(catalog, schema, tableName)) {
                    while (rs.next()) {
                        String idName = rs.getString(COLUMN_NAME);
                        desc.addIdName(idName);
                    }
                }

                try( ResultSet rs = metaData.getColumns(catalog, schema, tableName, Separator.LIKE)) {
                    while (rs.next()) {
                        String colName = rs.getString(COLUMN_NAME);
                        Integer sqlType = rs.getInt(DATA_TYPE);
                        Integer size = rs.getInt(COLUMN_SIZE);
                        Object o = rs.getObject(DECIMAL_DIGITS);

                        Integer digit = null;
                        if (o != null) {
                            digit = ((Number) o).intValue();
                        }

                        String remark = rs.getString(REMARKS);
                        ColDesc col = new ColDesc(colName, sqlType, size, digit, remark);
                        try {
                            if (checkAuto) {
                                String auto = rs.getString(IS_AUTOINCREMENT);
                                if (auto.equals(YES)) {
                                    col.setAuto(Boolean.TRUE);
                                }
                            }

                        } catch (SQLException ex) {
                            //某些驱动可能不支持
                            checkAuto = false;
                        }

                        desc.addCols(col);
                    }
                }
                return desc;

            } catch (SQLException e) {
                throw new OrmCoreException(OrmErrorCode.SQL_EXCEPTION, e);
            }
        }


    }

    private synchronized void initMetadata() {
        if (map != null) {
            return;
        }
        ThreadSafeCaseInsensitiveHashMap tempMap = new ThreadSafeCaseInsensitiveHashMap();
        try(Connection   conn = connectionSource.getMetaData();) {
            DatabaseMetaData databaseMetaData = conn.getMetaData();
            String catalog = this.defaultCatalog;
            String schema = this.defaultSchema;

            String namePattern = this.getTableNamePattern(databaseMetaData);
            ResultSet rs = databaseMetaData.getTables(catalog, schema, namePattern,TYPES);
            while (rs.next()) {
                String name = rs.getString(TABLE_NAME);
                String remarks = rs.getString(REMARKS);
                TableDesc desc = new TableDesc(name, remarks);
                desc.setSchema(this.defaultSchema);
                desc.setCatalog(catalog);
                tempMap.put(desc.getName(), desc);
                if (!tableVirtualMap.isEmpty() && tableVirtualMap.containsKey(name)) {
                    TableDesc newDesc = this.copyVirtualTable((String) tableVirtualMap.get(name), desc);
                    tempMap.put(newDesc.getName(), newDesc);
                }
            }

            rs.close();
            this.map = tempMap;
        } catch (SQLException e) {
            throw new OrmCoreException(OrmErrorCode.SQL_EXCEPTION, e);
        }
    }

    private TableDesc copyVirtualTable(String virtualName, TableDesc desc) {
        TableDesc newDesc = new TableDesc(virtualName, desc.getRemark());
        newDesc.setRealTableName(desc.getName());
        newDesc.setCatalog(desc.getCatalog());
        newDesc.setSchema(desc.getSchema());
        return newDesc;
    }

    private TableDesc initOtherSchemaTable(String sc, String table) {
        try (   Connection conn = this.connectionSource.getMetaData();){

            DatabaseMetaData metaData = conn.getMetaData();


            String catalog = this.getDbCatalog(sc);
            String schema = this.getDbSchema(sc);

            TableDesc desc = null;
            try(ResultSet rs = metaData.getTables(catalog, schema, getDbTableName(table),TYPES)) {

                while (rs.next()) {
                    String name = rs.getString(TABLE_NAME);
                    String remarks = rs.getString(REMARKS);
                    desc = new TableDesc(name, remarks);
                    desc.setSchema(sc);
                    desc.setCatalog(catalog);
                    map.put(sc + Separator.DOT + table, desc);
                }
            }
            if (desc != null) {
                return desc;
            } else {
                map.put(schema + Separator.DOT + table, NOT_EXIST);
                return null;
            }

        } catch (SQLException e) {
            throw new OrmCoreException(OrmErrorCode.SQL_EXCEPTION, e);
        }
    }

    private void close(Connection conn) {
        try {
            if (!connectionSource.isTransaction()) {
                if (conn != null) {
                    conn.close();
                }
            }

        } catch (Exception ex) {
            ex.printStackTrace();
        }

    }

    private void initDefaultSchema() {
        this.defaultSchema = this.sqlManager.getDefaultSchema();
        if (defaultSchema == null) {
            Connection conn = connectionSource.getMetaData();

            try {
                setDefaultSchema(conn);
                conn.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }


    }

    private void setDefaultSchema(Connection conn) throws SQLException {
        try {
            this.defaultCatalog = conn.getCatalog();
        } catch (Throwable e) {
            // jdbc低版本不支持
        }
        try {
            this.defaultSchema = conn.getSchema();
        } catch (Throwable e) {
            // jdbc低版本不支持
            String dbName = sqlManager.getDialectType().getName();
            if (dbName.equals(POSTGRES)) {
                defaultSchema = POSTGRES_PUBLIC;
            } else if (dbName.equals(SQL_SERVER)) {
                defaultSchema = SQL_SERVER_DBO;
            } else if (dbName.equals(ORACLE)) {
                defaultSchema = conn.getMetaData().getUserName();
            } else {
                defaultSchema = null;
            }

        }

    }

    /**
     * 按照我理解，对于访问表xx.yyy, 不同数据库有不同的catalog和schema
     *
     * @param namespace
     * @return
     */
    private String getDbSchema(DatabaseMetaData dbmd, String namespace) {
        if (DialectType.MYSQL.name().equals(this.dialectType)) {
            return null;
        } else if (DialectType.ORACLE.name().equals(this.dialectType)) {
            return namespace.toUpperCase();
        } else {
            return namespace;
        }
    }

    private String getTableNamePattern(DatabaseMetaData meta) throws SQLException {
        //mysql 6 是个在开发版本，有问题，不支持
        String p = meta.getDatabaseProductName();

        if (p.equalsIgnoreCase(MY_SQL)) {
            int c = meta.getDriverMajorVersion();
            if (c == MY_SQL_VERSION_6) {
                return Separator.LIKE;
            }
        }

        return null;
    }

    private String getDbSchema(String namespace) {
        if (dialectType.equals(MY_SQL)) {
            return null;
        } else if (dialectType.equals(ORACLE)) {
            return namespace.toUpperCase();
        } else {
            return namespace;
        }
    }

    private String getDbCatalog(String schema) {
        if (dialectType.equals(MY_SQL)) {
            return schema;
        } else {
            return null;
        }
    }

    private String getDbTableName(String name) {
        if (dialectType.equals(ORACLE)) {
            return name.toUpperCase();
        } else {
            return name;
        }
    }

    /**
     * 真表和假表
     *
     * @return
     */
    public Map<String, String> getTableVirtualMap() {
        return tableVirtualMap;
    }

    /***
     *
     * @param realTable
     * @param virtual
     */
    public void addTableVirtual(String realTable, String virtual) {
        this.tableVirtualMap.put(realTable, virtual);
        if (this.map == null) {
            return;
        }

        if (this.map.containsKey(realTable)) {
            //所有表信息加载，但还未加载具体表信息，加载时候会根据这个关系创建一个TableDesc
            return;
        }

        TableDesc desc = (TableDesc) this.map.get(realTable);
        TableDesc virtualTableDesc = new TableDesc(virtual, desc.getRemark());
        virtualTableDesc.setRealTableName(realTable);
        return;
    }
}
